package boardgamer.yavalath.server;

import boardgamer.common.CyclicIterator;
import boardgamer.common.net.PlayerId;
import boardgamer.yavalath.common.YavalathBoard;
import boardgamer.yavalath.common.YavalathException;
import boardgamer.yavalath.common.YavalathGameState;
import boardgamer.yavalath.common.YavalathGameState.Phase;
import boardgamer.yavalath.common.YavalathGameStateChange;
import boardgamer.yavalath.common.YavalathGameStateChange.PlayerAddToken;
import boardgamer.yavalath.common.YavalathGameStateChange.PlayerLoses;
import boardgamer.yavalath.common.YavalathGameStateChange.PlayerWins;
import boardgamer.yavalath.common.YavalathTile;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

import javax.annotation.Nullable;
import java.util.List;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

/**
 * TODO(sfraim) Move tests from {@link YavalathGameServerImplTest}
 */
public class YavalathGameStateUpdater {

  //TODO refactor this into helper methods
  public static ImmutableList<Pair<YavalathGameState, YavalathGameStateChange>>
      getUpdatedGameStateAddToken(YavalathGameState state, PlayerId player, int x, int y) throws YavalathException {
    ImmutableList.Builder<Pair<YavalathGameState, YavalathGameStateChange>> updatesBuilder = ImmutableList.builder();
    // Make sure that operation is allowed
    checkState(state.getPhase() == Phase.GAME,
        "Cannot add token in state: " + state.getPhase());

    checkArgument(state.getCurrentPlayer().equals(player), "It not player " + player + "'s turn");

    final YavalathBoard board = state.getBoard();
    checkArgument(board.hasTile(x, y), "Tile (" + x + "," + y + ") does not exist");

    if (!state.isSwappingAllowed() && board.getTile(x, y).hasOwner()) {
      throw YavalathException.invalidMove();
    }

    CyclicIterator<PlayerId> nextPlayers = CyclicIterator.copyOf(state.getActivePlayers());
    nextPlayers.next();
    validateForcedMove(x, y, state, nextPlayers);

    // Make the move
    YavalathGameState.Builder gameStateBuilder = YavalathGameState.Builder.fromPrototype(state);
    gameStateBuilder.activePlayers(nextPlayers);

    YavalathBoard newBoard = YavalathBoard.copyOf(board);
    newBoard.setToken(player, x, y);
    gameStateBuilder.board(newBoard);

    // Add the next winning moves
    gameStateBuilder.setPlayerWinningtiles(player, newBoard.nextWinningTiles(player, x, y));

    YavalathGameState newGameState = gameStateBuilder.build();
    updatesBuilder.add(new Pair<YavalathGameState, YavalathGameStateChange>(
        newGameState, new PlayerAddToken(player, x, y)));

    // Check if after the game ends after this move.
    List<YavalathTile> winningTiles = newBoard.winningTiles(player, x, y);
    List<YavalathTile> losingTiles = newBoard.losingTiles(player, x, y);
    if (winningTiles != null) {
      Pair<YavalathGameState, YavalathGameStateChange> update = endWithWin(player, newGameState, winningTiles);
      newGameState = update.first;
      updatesBuilder.add(update);
    } else {
      if (losingTiles != null) {
        Pair<YavalathGameState, YavalathGameStateChange> update
            =  playerLoses(player, newGameState, losingTiles);
        newGameState = update.first;
        updatesBuilder.add(update);
      }

      // Check if there is one player left
      if (newGameState.getActivePlayers().size() < 2) {
        Pair<YavalathGameState, YavalathGameStateChange> update =  endWithWin(newGameState.getActivePlayers().peek(),
            newGameState, null);
        newGameState = update.first;
        updatesBuilder.add(update);
      } else if (newBoard.getUnownedTilesCount() == 0) {
        Pair<YavalathGameState, YavalathGameStateChange> update =  endWithDraw(newGameState);
        newGameState = update.first;
        updatesBuilder.add(update);
      }
    }

    return updatesBuilder.build();
  }

  @VisibleForTesting
  static void validateForcedMove(int x, int y, YavalathGameState state, CyclicIterator<PlayerId> nextPlayers)
      throws YavalathException {
    // If there are 3 active players, check if the next player has a next winning move
    // If they do, make sure that the current player takes one of them
    if (state.getActivePlayers().size() >= 3) {
      PlayerId nextPlayer = nextPlayers.peek();
      Iterable<YavalathTile> nextWinningMoves = state.getNextWinningMoves(nextPlayer);

      // Remove tiles that have been owned
      final YavalathBoard boardCopy = YavalathBoard.copyOf(state.getBoard());
      Iterable<YavalathTile> unownedWinningMoves = Iterables.filter(
          nextWinningMoves,
          new Predicate<YavalathTile>() {
            @Override
            public boolean apply(YavalathTile input) {
              return !boardCopy.getTile(input.getX(), input.getY()).hasOwner();
            }
          });

      if (!Iterables.isEmpty(unownedWinningMoves)
          && !Iterables.contains(unownedWinningMoves, new YavalathTile(x, y))) {
        throw YavalathException.hasForcedMove();
      }
    }
  }

  static private Pair<YavalathGameState, YavalathGameStateChange> endWithDraw(YavalathGameState newGameState) {
    newGameState = YavalathGameState.Builder.fromPrototype(newGameState)
        .activePlayers(new CyclicIterator<PlayerId>())
        .phase(Phase.END_GAME)
        .build();
    return new Pair<YavalathGameState, YavalathGameStateChange>(newGameState, new YavalathGameStateChange.Draw());
  }

  static private Pair<YavalathGameState, YavalathGameStateChange> playerLoses(PlayerId player, YavalathGameState newGameState,
                                        List<YavalathTile> losingTiles) {
    CyclicIterator<PlayerId> activePlayers = CyclicIterator.copyOf(newGameState.getActivePlayers());
    activePlayers.removeFirstOccurence(player);
    newGameState = YavalathGameState.Builder.fromPrototype(newGameState)
        .activePlayers(activePlayers)
        .build();
    return new Pair<YavalathGameState, YavalathGameStateChange>(newGameState, new PlayerLoses(player, losingTiles));
  }

  static private Pair<YavalathGameState, YavalathGameStateChange> endWithWin(final PlayerId player, YavalathGameState newGameState,
                                       @Nullable List<YavalathTile> winningTiles) {
    CyclicIterator<PlayerId> activePlayers = CyclicIterator.copyOf(newGameState.getActivePlayers());
    // Remove losing players
    // TODO(sfraim) remove all players and see if adding playerId to certain change states makes sense?
    while (activePlayers.size() != 1) {
      if (!activePlayers.peek().equals(player)) {
        activePlayers.remove();
      } else {
        activePlayers.next();
      }
    }

    newGameState = YavalathGameState.Builder.fromPrototype(newGameState)
        .activePlayers(activePlayers)
        .phase(Phase.END_GAME)
        .build();
    return new Pair<YavalathGameState, YavalathGameStateChange>(newGameState, new PlayerWins(player, winningTiles));
  }
  
  // TODO get rid of this (only to get test to work - in a rush)
  public static class Pair<S, T> {
    public S first;
    public T second;
    
    public Pair(S s, T t) {
     first = s;
     second = t;
    }
  }
}
